﻿using System;
using System.Runtime.InteropServices;
using Un4seen.Bass;
using Un4seen.Bass.AddOn.Tags;
using System.Threading.Tasks;

// SIRP Experimental Firmware
// InetStream abstraction layer to BASS functionality
// (c) 2014 Chris Burri. MIT License applies. 
// See http://niston.wordpress.com

namespace sdd.subsystems.audio
{
    public class InetStream
    {

    #region Member Vars
        private int _stream;
        private string _streamUrl;
        private bool _raisePlayingEvent = false;

        private SYNCPROC _SyncMeta;
        private int _SyncMetaHandle = 0;
        private SYNCPROC _SyncStall;
        private int _SyncStallHandle = 0;
        private SYNCPROC _SyncEnd;
        private int _SyncEndHandle = 0;
        private SYNCPROC _SyncFree;
        private int _SyncFreeHandle = 0;
        private SYNCPROC _SyncSlide;
        private int _SyncSlideHandle = 0;
        private DOWNLOADPROC _DpStatus;


        private TAG_INFO _Tags;
        private System.Timers.Timer DelayedReadMetaTimer = new System.Timers.Timer();

        private int META_INITIAL_READ_DELAY = 500;                                
        
    #endregion
        
    #region Constructor/Destructor
        public AudioStreamer Parent { get; set; }

        public InetStream()
        {
            _Tags = new TAG_INFO();
            DelayedReadMetaTimer.Elapsed += DelayedReadMeta_Elapsed;
        }

        ~InetStream()
        {
            // disable timer for delayed reading of meta information
            DelayedReadMetaTimer.Elapsed -= DelayedReadMeta_Elapsed;            
            DelayedReadMetaTimer.Enabled = false;
        }
    #endregion

    #region Public Interface
        public event EventHandler StreamStart;
        public event EventHandler StreamHTTP;
        public event EventHandler StreamPlaying;
        public event EventHandler StreamEnd;
        public event EventHandler<StreamStallEventArgs> StreamStall;

        public event EventHandler MetadataUpdated;

        public string URL { get { return _streamUrl; } }

        public void Open(string streamURL)
        {
            _streamUrl = streamURL;
            Open();
        }

        public int Handle { get {return _stream; }}
        
        public void OpenAsync()
        {
            Task OpenTask = Task.Factory.StartNew(() => Open());
        }

        public void Open()
        {
            // set downloadproc
            _DpStatus = new DOWNLOADPROC(DownloadProc);

            // allow Playing event to occur once
            _raisePlayingEvent = true;

            // create stream from URL
            BASSFlag flags = BASSFlag.BASS_STREAM_DECODE | BASSFlag.BASS_STREAM_STATUS | (Parent.FloatingPointSupport ? BASSFlag.BASS_SAMPLE_FLOAT : 0);
            int newStream = Bass.BASS_StreamCreateURL(_streamUrl, 0,  flags, _DpStatus, IntPtr.Zero);
            if (newStream == 0)
            {
                // FAIL.
                throw new BASSException();
            }
            else
            {
                // now this is our stream
                _stream = newStream;

                // create synchronizers
                _SyncMeta = new SYNCPROC(MetaSyncProc);
                _SyncStall = new SYNCPROC(StallSyncProc);
                _SyncEnd = new SYNCPROC(EndSyncProc);
                _SyncSlide = new SYNCPROC(SlideSyncProc);
                _SyncFree = new SYNCPROC(FreeSyncProc);

                // set SYNCs
                _SyncMetaHandle = Bass.BASS_ChannelSetSync(_stream, BASSSync.BASS_SYNC_META, 0, _SyncMeta, IntPtr.Zero);
                _SyncStallHandle = Bass.BASS_ChannelSetSync(_stream, BASSSync.BASS_SYNC_STALL, 0, _SyncStall, IntPtr.Zero);
                _SyncEndHandle = Bass.BASS_ChannelSetSync(_stream, BASSSync.BASS_SYNC_END, 0, _SyncEnd, IntPtr.Zero);
                _SyncSlideHandle = Bass.BASS_ChannelSetSync(_stream, BASSSync.BASS_SYNC_SLIDE, 0, _SyncSlide, IntPtr.Zero);
                _SyncFreeHandle = Bass.BASS_ChannelSetSync(_stream, BASSSync.BASS_SYNC_FREE, 0, _SyncFree, IntPtr.Zero);

                // raise Start event
                if (StreamStart != null) { StreamStart(this, new EventArgs()); }
            }
        }

        public void Stop(bool stopImmediately = false)
        {
            //
            if (stopImmediately)
            {
                // force stop and free
                Bass.BASS_ChannelStop(_stream);
                Bass.BASS_StreamFree(_stream);
            }
            else
            {
                // fade out and stop (autofree)
                Bass.BASS_ChannelSlideAttribute(_stream, BASSAttribute.BASS_ATTRIB_VOL, -1, 600);
            }
        }
           
        // channel status is meaningless with decoding channels
        public PlayerStatuses Status
        {
            get
            {
                switch (Bass.BASS_ChannelIsActive(_stream))
                {
                    case BASSActive.BASS_ACTIVE_PLAYING:
                        return PlayerStatuses.Playing;
                    case BASSActive.BASS_ACTIVE_PAUSED:
                        return PlayerStatuses.Paused;
                    case BASSActive.BASS_ACTIVE_STALLED:
                        return PlayerStatuses.Stalled;
                    default:
                        return PlayerStatuses.Stopped;
                }
            }
        }

        public AudioLevels AudioLevels
        {
            get
            {
                // get audio levels (L/R)
                return new AudioLevels(Bass.BASS_ChannelGetLevel(_stream));
            }
        }

        public int BufferLevel
        {
            get
            {
                // get buffer level
                long len = Bass.BASS_StreamGetFilePosition(_stream, BASSStreamFilePosition.BASS_FILEPOS_END);
                long down = Bass.BASS_StreamGetFilePosition(_stream, BASSStreamFilePosition.BASS_FILEPOS_DOWNLOAD);
                long dec = Bass.BASS_StreamGetFilePosition(_stream, BASSStreamFilePosition.BASS_FILEPOS_CURRENT);
                int percent = (int)(Math.Floor(((float)(down) - (float)(dec)) * (float)(100) / (float)(len)));
                if (percent > 100) { percent = 100; }; if (percent < 0) { percent = 0; }
                return percent;
            }
        }

        public int BitsPerSample
        {
            get
            {
                // get channel information
                BASS_CHANNELINFO chanInf = Bass.BASS_ChannelGetInfo(_stream);
                // 32bit? 8bit? 16bit.
                if (chanInf != null) { return (chanInf.Is32bit ? 32 : (chanInf.Is8bit ? 8 : 16)); } else { return 0;  }
            }
        }

        public int SampleRate
        {
            get
            {
                // get channel information
                BASS_CHANNELINFO chanInf = Bass.BASS_ChannelGetInfo(_stream);
                // 32bit? 8bit? 16bit.
                if (chanInf != null) { return chanInf.freq; } else { return 0; }
            }
        }

        public int NumberOfChannels
        {
            get
            {
                // get channel information
                BASS_CHANNELINFO chanInf = Bass.BASS_ChannelGetInfo(_stream);
                if (chanInf != null) { return chanInf.chans; } else { return 0; }
            }
        }

        public BASSChannelType ChannelType
        {
            get
            {
                // get channel information
                BASS_CHANNELINFO chanInf = Bass.BASS_ChannelGetInfo(_stream);
                if (chanInf != null) { return chanInf.ctype; } else { return BASSChannelType.BASS_CTYPE_UNKNOWN; }
            }
        }

        public int Bitrate
        {
            get
            {
                // get station and bitrate from ICY tags
                int bitRateInt = 0;
                string bitRate = "";
                string[] icyInfo = Bass.BASS_ChannelGetTagsICY(_stream);
                if (icyInfo != null)
                {
                    for (int n = 0; n <= icyInfo.GetUpperBound(0); n++)
                    {
                        // found bit rate
                        if (icyInfo[n].Contains("icy-br")) { bitRate = icyInfo[n].Replace("icy-br:", ""); }
                    }
                }
                return (int.TryParse(bitRate, out bitRateInt) ? bitRateInt : 0);
            }
        }

        public string Station
        {
            get
            {
                // get station and bitrate from ICY tags
                string stationName = "";
                string[] icyInfo = Bass.BASS_ChannelGetTagsICY(_stream);
                if (icyInfo != null)
                {
                    for (int n = 0; n <= icyInfo.GetUpperBound(0); n++)
                    {
                        // found station name
                        if (icyInfo[n].Contains("icy-name")) { stationName = icyInfo[n].Replace("icy-name:", ""); }
                    }
                }
                return stationName;
            }
        }

        public string Artist
        {
            get { return _Tags.artist; }
        }

        public string Title
        {
            get { return _Tags.title; }
        }

    #endregion

    #region Private Functions
        private void UpdateMetadataFromTags()
        {
            var tags = Bass.BASS_ChannelGetTags(_stream, BASSTag.BASS_TAG_META);
            if (tags != null) { _Tags.UpdateFromMETA(tags, true, true); }
            if (MetadataUpdated != null) { MetadataUpdated(this, new EventArgs()); }
        }

        #region SyncProcs
        // SYNC procs
        private void MetaSyncProc(int syncHandle, int channel, int data, IntPtr user)
        {
            // refresh metadata
            UpdateMetadataFromTags();
        }

        private void StallSyncProc(int syncHandle, int channel, int data, IntPtr user)
        {
            // stall/resume event
            if (StreamStall != null) { StreamStall(this, new StreamStallEventArgs(data == 1 ? true : false)); }
        }

        private void EndSyncProc(int syncHandle, int channel, int data, IntPtr user)
        {
            // stream has ended
            if (StreamEnd != null) { this.StreamEnd(this, new EventArgs()); }
        }

        private void SlideSyncProc(int syncHandle, int channel, int data, IntPtr user)
        {
            // slide completed
            // System.Diagnostics.Debug.Print("SlideSync");
        }

        private void FreeSyncProc(int syncHandle, int channel, int data, IntPtr user)
        {
            // stream has been freed
            //System.Diagnostics.Debug.Print("FreeSync");
            if (StreamEnd != null) { this.StreamEnd(this, new EventArgs()); }
        }

        private void DownloadProc(IntPtr buffer, int length, IntPtr user)
        {
            // http information
            if (length == 0)
            {
                string[] data = Utils.IntPtrToArrayNullTermAnsi(buffer);
                if (data != null)
                {
                    foreach (string line in data)
                    {
                        if (line.ToUpper().StartsWith("HTTP"))
                        {
                            System.Diagnostics.Debug.Print(line);
                            if (line.ToUpper().Contains("200 OK"))
                            {
                                if (StreamHTTP != null) { StreamHTTP(this, new EventArgs()); }
                            }
                        }
                    }
                }
            }
            else
            {
                // only raise this once
                if (_raisePlayingEvent)
                {
                    if (StreamPlaying != null & _stream != 0)
                    {
                        // first time we read valid stream data
                        // enable delayed meta tag reading
                        DelayedReadMetaTimer.Interval = META_INITIAL_READ_DELAY;
                        DelayedReadMetaTimer.Enabled = true;
                        // raise playing event
                        StreamPlaying(this, new EventArgs()); 
                        _raisePlayingEvent = false;
                    }
                }
            }
        }

        void DelayedReadMeta_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
        {
            DelayedReadMetaTimer.Enabled = false;
            UpdateMetadataFromTags();
        }


        // This method also destroys each unmanaged character pointers and will also destroy the array itself.
        //static void MarshalUnmananagedStrArray2ManagedStrArray
        //(
        //  IntPtr pUnmanagedStringArray,
        //  int StringCount,
        //  out string[] ManagedStringArray
        //)
        //{
        //    IntPtr[] pIntPtrArray = new IntPtr[StringCount];
        //    ManagedStringArray = new string[StringCount];

        //    Marshal.Copy(pUnmanagedStringArray, pIntPtrArray, 0, StringCount);

        //    for (int i = 0; i < StringCount; i++)
        //    {
        //        ManagedStringArray[i] = Marshal.PtrToStringAnsi(pIntPtrArray[i]);
        //        Marshal.FreeCoTaskMem(pIntPtrArray[i]);
        //    }

        //    Marshal.FreeCoTaskMem(pUnmanagedStringArray);
        //}


        #endregion
    #endregion
    
        #region NestedTypes
        public enum PlayerStatuses
        {
            Stopped = 0,
            Playing = 1,
            Paused = 2,
            Stalled = 3
        }

        public enum StreamTypes
        {
            Unknown = 0,
            MP3 = 1,
            OGG = 2,
            AAC = 3,
            OPUS = 4
        }
       
        public class StreamStallEventArgs : EventArgs
        {
            public bool StreamHasResumed { private set; get; }
            public StreamStallEventArgs(bool streamHasResumed)
            {
                this.StreamHasResumed = streamHasResumed;
            }
        }
        #endregion
    }
}
